package cn.lg.soar.system.biz.modules.monitor.service.impl;

import cn.lg.soar.common.algorithm.HmacSigner;
import cn.lg.soar.common.util.AssertUtil;
import cn.lg.soar.common.util.DatetimeUtil;
import cn.lg.soar.common.util.IpUtil;
import cn.lg.soar.common.util.ParameterUtil;
import cn.lg.soar.common.util.current.CurrentProxy;
import cn.lg.soar.common.util.current.CurrentTokenInfo;
import cn.lg.soar.common.util.current.ThreadLocalHolder;
import cn.lg.soar.common.util.data.DataUtil;
import cn.lg.soar.common.util.data.RandomUtil;
import cn.lg.soar.common.util.data.SecureRandomUtil;
import cn.lg.soar.common.util.token.LoginInfo;
import cn.lg.soar.common.util.token.SoarJWT;
import cn.lg.soar.core.config.cache.template.ICacheTemplate;
import cn.lg.soar.core.manager.IStateAccessTokenManager;
import cn.lg.soar.mvc.util.ServletUtils;
import cn.lg.soar.system.api.enums.OnlineModeEnum;
import cn.lg.soar.system.api.model.TokenInfoDTO;
import cn.lg.soar.system.biz.config.SystemProps;
import cn.lg.soar.system.biz.modules.auth.service.ITokenCurrBindService;
import cn.lg.soar.system.biz.modules.monitor.entity.RefreshToken;
import cn.lg.soar.system.biz.modules.monitor.mapper.TokenInfoMapper;
import cn.lg.soar.system.biz.modules.monitor.model.RefreshTokenRecord;
import cn.lg.soar.system.biz.modules.monitor.service.IRefreshTokenService;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import eu.bitwalker.useragentutils.UserAgent;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;

import java.io.Serializable;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * refreshToken状态管理(可根据自己的需求修改)
 * @author luguoxiang
 * 开源项目：https://gitee.com/lgx1992/lg-soar 求star！请给我star！请帮我点个star！
 */
public class RefreshTokenServiceImpl extends ServiceImpl<TokenInfoMapper, RefreshToken> implements IRefreshTokenService, CurrentProxy<RefreshTokenServiceImpl> {

    @Autowired
    private ITokenCurrBindService tokenCurrBindService;
    @Autowired
    private ICacheTemplate<String, String> cacheTemplate;
    @Autowired
    private IStateAccessTokenManager stateAccessTokenManager;

    private SystemProps properties;
    private long accessTokenExpires;
    private long shortRefreshTokenExpires;
    private String refreshTokenSecret;
    private OnlineModeEnum mode;
    private final Cache<Long, SoarJWT> soarJWTCache = Caffeine.newBuilder()
            .maximumSize(128)
            .initialCapacity(16)
            .expireAfterWrite(Duration.ofMinutes(30))
            .build();

    @Autowired
    public void setProperties(SystemProps properties) {
        this.properties = properties;
        this.accessTokenExpires = properties.getAccessTokenExpire().getSeconds() * 1000;
        this.shortRefreshTokenExpires = this.accessTokenExpires + 600_000;
        if (properties.getRefreshTokenSecret() == null) {
            throw new RuntimeException("请配置：lg.system.refresh-token-secret");
        }
        this.refreshTokenSecret = properties.getRefreshTokenSecret();
        this.mode = properties.getOnlineMode();
    }

    @Override
    public TokenInfoDTO create(LoginInfo loginInfo, long expires) {
        long userId = loginInfo.getUserId();
        int clientType = loginInfo.getClientType();
        // 同时在线控制
        switch (mode) {
            case only:
                removeByUserId(userId);
                break;
            case clientType:
                removeByUserId(userId, clientType);
                break;
            default:break;
        }
        // 创建刷新token
        RefreshToken refreshToken = new RefreshToken();
        refreshToken.setUserId(userId);
        refreshToken.setUserType(loginInfo.getUserType());
        refreshToken.setClientType(clientType);
        refreshToken.setDuration(expires);
        // 3.返回信息
        return doCreate(refreshToken, loginInfo.getTenantId());
    }

    @Override
    public TokenInfoDTO create(LoginInfo loginInfo, boolean rememberMe) {
        Integer clientType = loginInfo.getClientType();
        long expires;
        if (rememberMe) {
            expires = properties.getRefreshTokenExpireMillis(clientType);
        } else {
            expires = shortRefreshTokenExpires;
        }
        // 创建刷新token
        return create(loginInfo, expires);
    }

    @Override
    public TokenInfoDTO create(Long userId, long expires) {
        LoginInfo loginInfo = new LoginInfo(userId, 0);
        return create(loginInfo, expires);
    }

    @Override
    public TokenInfoDTO create(Long userId, boolean rememberMe) {
        LoginInfo loginInfo = new LoginInfo(userId, 0);
        return create(loginInfo, rememberMe);
    }

    @Override
    public RefreshTokenRecord verify(String refreshToken) {
        // 验证token串有效性
        String payload = getSoarJWT(shortRefreshTokenExpires).verify(refreshToken);
        ParameterUtil.notNull(payload, "refresh token 无效");
        String[] split = payload.split("\\.");
        long tokenId = Long.parseLong(split[0]);
        int tokenValue = Integer.parseInt(split[1]);
        int tenantId = Integer.parseInt(split[2]);
        // 验证token状态
        RefreshToken tokenInfo = getById(tokenId);
        ParameterUtil.notNull(tokenInfo, "refresh token 无效");
        // 验证tokenInfo是否属于当前token
        ParameterUtil.equals(tokenInfo.getValue(), tokenValue, "refresh token 无效");
        return new RefreshTokenRecord(tokenInfo, tenantId);
    }

    @Override
    public TokenInfoDTO refresh(String refreshToken) {
        // 验证token有效性
        RefreshTokenRecord verify = verify(refreshToken);
        // 3.返回信息
        return doCreate(verify.refreshToken(), verify.tenantId());
    }

    @Override
    public TokenInfoDTO changeTenant(long tokenId, int tenantId) {
        RefreshToken tokenInfo = getById(tokenId);
        ParameterUtil.notNull(tokenInfo, "refresh token 无效");
        return doCreate(tokenInfo, tenantId);
    }

    @Override
    public boolean removeById(Serializable id) {
        AssertUtil.notNull(id, "缺少id参数");
        AssertUtil.isTrue(super.removeById(id), "清除token失败");
        stateAccessTokenManager.removeToken((Long) id);
        return true;
    }

    @Override
    public boolean removeByIds(Collection<?> ids) {
        AssertUtil.notEmpty(ids, "缺少id参数");
        AssertUtil.isTrue(super.removeByIds(ids), "清除token失败");
        for (Object id : ids) {
            stateAccessTokenManager.removeToken((Long) id);
        }
        return true;
    }

    @Override
    public boolean removeByUserId(Serializable userId) {
        List<RefreshToken> list = this.list(Wrappers.<RefreshToken>lambdaQuery()
                .select(RefreshToken::getId)
                .eq(RefreshToken::getUserId, userId));
        if (DataUtil.isValuable(list)) {
            List<Long> ids = list.stream().map(RefreshToken::getId).collect(Collectors.toList());
            return removeByIds(ids);
        }
        return true;
    }

    @Override
    public boolean removeByUserId(Serializable userId, Integer clientType) {
        List<RefreshToken> list = this.list(
                Wrappers.<RefreshToken>lambdaQuery()
                        .select(RefreshToken::getId)
                        .eq(RefreshToken::getUserId, userId)
                        .eq(RefreshToken::getClientType, clientType)
        );
        if (DataUtil.isValuable(list)) {
            List<Long> ids = list.stream().map(RefreshToken::getId).collect(Collectors.toList());
            return removeByIds(ids);
        }
        return true;
    }

    @Scheduled(fixedDelay = 610000)
    public void optimize() {
        // 利用缓存（当使用redis时）实现部署多个微服务实例时，每十分钟只会执行一次
        if (cacheTemplate.putIfAbsent("refresh-token-optimize", "1", 600000)) {
            List<Long> ids = list(
                    Wrappers.<RefreshToken>lambdaQuery()
                            .select(RefreshToken::getId)
                            .le(RefreshToken::getRefreshExpires, LocalDateTime.now())
            ).stream()
                    .map(RefreshToken::getId)
                    .collect(Collectors.toList());
            if (!ids.isEmpty()) {
                tokenCurrBindService.removeByIds(ids);
                this.removeByIds(ids);
            }
        }
    }

    private TokenInfoDTO doCreate(RefreshToken token, int tenantId) {
        // 有效期
        long expires = token.getDuration();
        // 更新tokenValue
        Long tokenId = token.getId();
        int tokenValue = SecureRandomUtil.getInt();
        long millis = System.currentTimeMillis();
        if (tokenId == null) {
            HttpServletRequest request = ServletUtils.getRequest();
            int ip = IpUtil.getRemoteIpInt(request);
            UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
            //
            RefreshToken refreshTokenInfo = new RefreshToken();
            refreshTokenInfo.setUserId(token.getUserId());
            refreshTokenInfo.setUserType(token.getUserType());
            refreshTokenInfo.setClientType(token.getClientType());
            //
            refreshTokenInfo.setDuration(expires);
            refreshTokenInfo.setIp(ip);
            refreshTokenInfo.setSystem(userAgent.getOperatingSystem().getName());
            refreshTokenInfo.setBrowser(userAgent.getBrowser().getName());

            tokenId = IdWorker.getId();
            refreshTokenInfo.setId(tokenId);
            refreshTokenInfo.setValue(tokenValue);
            refreshTokenInfo.setAccessExpires(DatetimeUtil.from(millis + accessTokenExpires));
            refreshTokenInfo.setRefreshExpires(DatetimeUtil.from(millis + expires));
            AssertUtil.isTrue(this.save(refreshTokenInfo), "保存token信息失败");
        } else {
            RefreshToken refreshTokenInfo = new RefreshToken();
            refreshTokenInfo.setId(tokenId);
            refreshTokenInfo.setValue(tokenValue);
            refreshTokenInfo.setRefreshExpires(DatetimeUtil.from(millis + expires));
            refreshTokenInfo.setAccessExpires(DatetimeUtil.from(millis + accessTokenExpires));
            AssertUtil.isTrue(this.updateById(refreshTokenInfo), "保存token信息失败");
        }
        // 更新
        String random = SecureRandomUtil.getString(64);
        // 生成新的token
        String refreshToken = getSoarJWT(expires).create(tokenId +"."+ tokenValue +"."+ tenantId +"."+ random);
        // 3.返回信息
        LoginInfo loginInfo = new LoginInfo(token.getUserId(), token.getUserType());
        loginInfo.setClientType(token.getClientType());
        loginInfo.setTenantId(tenantId);
        CurrentTokenInfo currentTokenInfo = CurrentTokenInfo.create(tokenId, loginInfo);
        ThreadLocalHolder.setCurrentUser(currentTokenInfo);
        // 生成访问token
        String accessToken = stateAccessTokenManager.create(currentTokenInfo.toString());
        return new TokenInfoDTO(accessToken, refreshToken);
    }

    private SoarJWT getSoarJWT(long expires) {
        return soarJWTCache.get(expires, k -> new SoarJWT(HmacSigner.sha512(this.refreshTokenSecret), expires));
    }
}
